💡 AI 인사이트

🤖 AI가 여기에 결과를 출력합니다...

댓글 커뮤니티

쿠팡이벤트

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

검색

    로딩 중이에요... 🐣

    [코담] 웹개발·실전 프로젝트·AI까지, 파이썬·장고의 모든것을 담아낸 강의와 개발 노트

    Django(todoList) 2 todolist 만들기 | ✅ 저자: 이유정(박사)

    [feat] django (Day 2)
    - rest API install 
    - serializers 
    - apis 
    - imsomnia
    
    - 파일 분리 : 템플릿 렌더링과 API를 분리해서 가독성과 유지보수를 
    좋게 하려는 목적
    - 단: 반드시 apis.py로 파일명을 만들 필요는 없다. views.py에 
    작성해도 된다.
    

    🔹 목록보기

    todo > urls.py

    from django.urls import path
    from . import views
    
    urlpatterns = [
    	path("list/", views.TodoListView.as_view(), name="todo_List"),
        # 실제 작동용 list
    ]
    

    todo > serializers.py

    from rest_framework.serializers import ModelSerializer
    from .models import Todo
    
    class TodoSerializer(ModelSerializer):
    	class Meta:
    		model = Todo
    		fields = "__all__" # 모델의 모든 필드를 자동으로 직렬화합니다
            
            fields = [
                "name",
                "description",
                "complete",
                "exp",
                "completed_at",
                "created_at",
                "updated_at"
            ]
            exclude = ["created_at", "updated_at"]
            # **모든 필드를 기본 포함시키고** → 특정 필드만 제외하고 싶을 때
    

    사용 안하고 싶은 필드는 안쓰면 자동 제외가 된다. 일부 필드만 사용하고 싶다 fields = ["필드1", "필드2"] 거의 모든 필드를 쓰는데 몇 개만 빼고 싶다 exclude = ["뺄_필드1", "뺄_필드2"]

    REST_FRAMEWORK = {
        'DEFAULT_RENDERER_CLASSES': [
            'rest_framework.renderers.JSONRenderer',
        ]
    }
    
    INSTALLED_APPS = [
    	'rest_framework',
    ]
    

    DRF가 HTML 렌더링 대신 JSON만 반환 http://127.0.0.1:8000/todo/api/list/

    todo/views.py

    from django.shortcuts import render
    from .models import Todo
    from django.views.generic import ListView
    from django.urls import reverse_lazy
    
    # 목록 조회
    class TodoListView(ListView):
        model = Todo
        template_name = "todo/list.html"
        context_object_name = "todos"
        ordering = ['-created_at']
        success_url = reverse_lazy('todo_List')
    

    templates/base.html

    {% load static %} 
    <!DOCTYPE html>
    <html lang="ko">
    <body>
    	{% block content %}{% endblock %}
    </body>
    </html>
    

    todo/list.html

    {% extends "base.html" %}
    {% load static %}
    {% block content %}
    
    <div class="todocontainer"> 
        {% for todo in todos %}
            <div class="todo-item">
                <p><strong>이름:</strong> {{ todo.name }}</p>
                <p><strong>설명:</strong> {{ todo.description }}</p>
                <p><strong>완료 여부:</strong> {{ todo.complete }}</p>
                <p><strong>작성일:</strong> 
                {{ todo.created_at|date:"Y-m-d H:i" }}</p>
                
                <hr>
            </div>
        {% empty %}
            <p>등록된 할 일이 없습니다.</p>
        {% endfor %}
    </div>
    <button class="todoCreate" id="createBtn">Todo 등록하기</button>
    
    <script>
        document.addEventListener("DOMContentLoaded", function () {
            console.log("create loading");
        });
    		    
    	document.getElementById("createBtn").addEventListener
    	("click", () => {
    		//window.location.href = "/todo/create/"; # 라우터주소
    		console.log("createBtn click")
    	});
    
    </script>
    {% endblock %}
    

    ("click", () => { ... }) 이 부분은 자바스크립트의 화살표 함수(Arrow Function) 문법입니다.

    화살표 함수 문법:

    () => {
      // 클릭 시 실행할 코드
    }
    
    • () : 매개변수 (없으면 비워둠)
    • => { ... } : 함수의 본문
    • 화살표 함수는 function() 키워드보다 간결한 문법입니다.

    동일한 의미의 일반 함수 방식:

    document.getElementById("createBtn").addEventListener("click", function () {
        window.location.href = "/todo/create/"; # 클릭후 이동할 URL
        console.log("createBtn click");
    });
    

    todo/urls.py

    from . import api_views
    
    path("api/list/", api_views.TodoListAPI.as_view(), name="todo_api_list"),
    

    todo > api_views.py

    from rest_framework.views import APIView
    from rest_framework.response import Response
    from .serializers import TodoSerializer
    from .models import Todo
    from rest_framework import status
    
    # 전체보기
    class TodoListAPI(APIView):
    	def get(self, request):
    		todos = Todo.objects.all() 
    		serializer = TodoSerializer(todos, many=True)
    		return Response(serializer.data)
    

    /todo/list/ 전체 Todo 목록 조회 http://127.0.0.1:8000/todo/api/list/


    🔹 생성하기

    todo/urls.py

    # 탬플릿View
    path("create/", views.TodoCreateView.as_view(), name="todo_Create"),
    
    # APIView
    path("api/create/", api_views.TodoCreateAPI.as_view(), name="todo_api_create"),
    

    todo/views.py

    from django.views.generic import ListView, CreateView
    
    # 생성
    class TodoCreateView(CreateView):
        model = Todo
        fields = ['name', 'description', 'complete', 'exp']
        template_name = "todo/create.html"
        success_url = reverse_lazy('todo_List')
    

    todo/api_views.py

    from rest_framework import status
    from rest_framework.views import APIView
    from rest_framework.response import Response
    from .serializers import TodoSerializer
    from .models import Todo
    
    # 생성하기
    class TodoCreateAPI(APIView):
    	def post(self, request):
    		serializer = TodoSerializer(data=request.data)
    		serializer.is_valid(raise_exception=True)
    		todo = serializer.save()
    		return Response(TodoSerializer(todo).data,
    		status=status.HTTP_201_CREATED)
    

    templates/create.html

    {% extends "base.html" %}
    {% load static %}
    {% block content %}
    <div class="container">
        <h2>Create a New Todo</h2>
            <div>
                <label for="name">Name:</label>
                <input type="text" name="name" id="name">
            </div>
    
            <div>
                <label for="description">Description:</label>
                <textarea name="description" id="description">
                </textarea>
            </div>
    
            <div>
                <label for="complete">Complete:</label>
                <input type="checkbox" name="complete" id="complete">
            </div>
    
            <div>
                <label for="exp">Experience Points:</label>
                <input type="number" name="exp" id="exp" min="0">
            </div>
    
            <button type="submit" id="todoCreate">Create</button>
    </div>
    
    <script>
        document.addEventListener("DOMContentLoaded", function () {
            console.log("create loading")
        });
    </script>
    {% endblock %}
    

    /todo/create/ 새로운 Todo 생성 http://127.0.0.1:8000/todo/api/create/ TodoCreateAPI POST

    테스트 데이터 Imsomnia

    {
    		"name": "멋쟁이호랑이처럼",
    		"description": "설명을 작성해줘",
    		"complete": false,
    		"exp": 10
    	}
    

    🔹 상세보기

    todo/urls.py

    # 탬플릿View
    path("detail/<int:pk>/", views.TodoDetailView.as_view(), name="todo_Detail"),
    
    # APIView
    path("api/retrieve/<int:pk>/", api_views.TodoRetrieveAPI.as_view(), name="todo_api_retrieve"),
    

    todo/views.py

    # 상세보기
    class TodoDetailView(DetailView):
        model = Todo
        template_name = "todo/detail.html"
        context_object_name = "todo"
    

    api_views.py

    # 상세보기
    class TodoRetrieveAPI(APIView):
        def get(self, request, pk):
            try:
                todo = Todo.objects.get(pk=pk)
            except Todo.DoesNotExist:
                return Response({"error":"해당하는 todo가 없습니다."},
                status=status.HTTP_404_NOT_FOUND)
            serializer = TodoSerializer(todo)
            return Response(serializer.data)
    

    templates/todo/detail.html

    {% extends "base.html" %}
    {% load static %}
    {% block content %}
    
    <div class="todoDetail"></div>
    <div class="btnList">
        <button class="todoUpdate">수정</button>
        <button class="todoDelete">삭제</button>
    </div>
    
    <script>
    </script>
    {% endblock %}
    

    /todo/retrieve/int:pk/ 특정 Todo 상세 조회 http://127.0.0.1:8000/todo/api/retrieve/1/ TodoRetrieveAPI GET


    🔹 리스트를 화면에 출력하기

    ✅ 아래 코드는 axios를 사용할 때 공통 설정을 미리 만들어서 계속 반복하지 않도록 도와주는 "axios 인스턴스 생성" 코드입니다.

    const axiosInstance = axios.create({
        baseURL: '/',    // API 베이스 URL
        timeout: 5000,
        headers: { 'Content-Type': 'application/json' }
    });
    
    항목 의미 예시
    baseURL 기본 요청 주소 /이면 /todo/api/ → 전체 URL은 /todo/api/list처럼 처리됨
    timeout 요청 시간 제한 (ms) 5000 = 5초 안에 응답 없으면 실패로 처리
    headers 요청 헤더 Content-Type은 서버에 JSON 보낸다는 의미
    baseURL이란?
    axios로 요청을 보낼 때 기본이 되는 주소입니다.
    이걸 지정해두면 모든 요청에 자동으로 붙습니다.
    const axiosInstance = axios.create({
      baseURL: '/api'
    });
    
    axiosInstance.get('/todos');  
    // 실제 요청 주소 → "/api/todos"
    

    baseURL은 URL의 공통 접두사 역할을 합니다.

    예시)

    baseURL: '/todo/viewsets'
    
    axiosInstance.get('/view/?page=1');
    // 실제 요청은 → /todo/viewsets/view/?page=1
    

    서버 주소: http://127.0.0.1:8000 요청하려는 경로: /todo/api/list/

    baseURL: '/' 설정한 경우

    const axiosInstance = axios.create({
      baseURL: '/',  // 루트부터 시작
    });
    
    http://127.0.0.1:8000/ + todo/api/list/ = http://127.0.0.1:8000/todo/api/list/
    

    안전하게 항상 루트(/)부터 시작, 현재 페이지 경로에 영향 받지 않음

    baseURL을 설정하지 않고 axios 기본 사용한 경우

    axios.get('todo/api/list/');
    

    브라우저에서 실행되는 위치가 중요합니다 예를 들어, 현재 페이지 URL이:

    http://127.0.0.1:8000/todo/
    

    이때 상대경로로 요청하면 실제 요청 주소는:

    http://127.0.0.1:8000/todo/todo/api/list/
    

    경로가 꼬입니다! (404 오류 가능성)이 있습니다.


    headers란?

    headers: {
      'Content-Type': 'application/json'
    }
    

    서버에 요청을 보낼 때, 요청 헤더(Request Header)에 추가되는 메타정보입니다.
    서버가 요청을 어떻게 해석할지를 알려줍니다. Content-Type': 'application/json : 서버에 JSON 형식으로 데이터를 보낸다는 의미


    ✅ 기본 틀 잡기 (HTML + 등록 버튼 이동)

    • 목표: 페이지가 열리고 등록하기 버튼만 동작하는지 확인
    • 테스트: 버튼 클릭 시 /todo/create/로 이동되는지만 보면 OK

    1. HTML 내용이 모두 화면에 나타날 때까지 기다렸다가 init() 함수를 실행해요

    document.addEventListener('DOMContentLoaded', init);
    

    HTML이 먼저 로드되고, JavaScript는 느리게 로드되기 때문에
    document.addEventListener('DOMContentLoaded', ...)를 사용하는 겁니다.


    2. 웹페이지가 처음 열릴 때 버튼 클릭 같은 UI 이벤트들을 등록하는 함수(UIEvents)를 실행하는 함수입니다.

    function init() {
        UIEvents(); // 1단계: 버튼 등의 UI에 클릭 이벤트 연결
        loadTodoList(); 
        // 2단계: Todo 목록의 페이지네이션 설정에 따라 나뉘는 
        // 첫번째 페이지를 뜻합니다.
    }
    

    init()는 페이지가 열릴 때 실행됩니다. init() 내부에서 UIEvents()라는 함수를 호출합니다.

    순서대로 해석하면: UIEvents(); 버튼 등 HTML 요소에 이벤트(클릭 등)를 연결해주는 함수입니다.


    3. "Todo 등록하기" 버튼에 클릭했을 때 어떤 동작을 할지 연결해요

    function UIEvents() {
        document.getElementById('createBtn')
            .addEventListener('click', onCreateClick);
    }
    

    4. 버튼을 클릭하면"/todo/create/"주소로 이동해요(Todo 작성 화면으로 이동)

    function onCreateClick() {
        window.location.href = '/todo/create/';
    }
    

    ✅ Todo 목록 1페이지만 불러오기

    5. 지정한 페이지 번호의 Todo 목록을 서버에서 받아와서 화면에 보여줘요

    function loadTodoList(page) {
        fetchTodoData(page)  // 서버에서 데이터를 받아오고
            .then(data => {
    
    			// 받아온 데이터에서 실제 todo 항목만 꺼내고
                const todos = extractTodoArray(data);
                renderTodoList(todos); // 화면에 출력해요
            })
            
            // 오류가 나면 콘솔에 출력해요
            .catch(err => console.error('리스트 로드 실패:', err));
    }
    

    6. axios를 이용해서 해당 페이지의 Todo 데이터를 서버로부터 가져와요

    function fetchTodoData(page) {
        return axiosInstance
            .get(`/todo/api/list/?page=${page}`)
            .then(res => res.data);
    }  
    

    이 코드는 page 번호를 쿼리 파라미터로 붙여 특정 페이지 데이터를 요청하는 것입니다.

    GenericAPIView + ListModelMixin 또는 ListAPIView 기반

    .get(`/todo/generics/list/?page=${page}`) //제너릭일때
    

    ViewSet + Router 자동 URL 패턴을 사용할 때 나오는 URL입니다.

    .get(`/todo/viewsets/view/?page=${page}`) //뷰셋일때
    

    7. 서버에서 받은 JSON 응답의 구조가 다양할 수 있기 때문에,
    그 중에서 실제로 필요한 Todo 목록 배열만 추출하는 역할을 합니다.

    function extractTodoArray(data) {
        if (Array.isArray(data.data)) return data.data;
        if (Array.isArray(data.results)) return data.results;
        return []; 
    }
    

    이 함수의 목적은? 서버에서 받은 응답이 어떤 구조든 간에, 실제 Todo 리스트 배열만 안전하게 꺼내기 위한 유틸 함수입니다.

    페이지네이션이 없는 경우 서버응답:

    [
      { "id": 1, "name": "공부하기" },
      { "id": 2, "name": "청소하기" }
    ]
    

    이건 단순한 리스트 (Array)로 조건 Array.isArray(data)true가 되므로 바로 return data; 실행됨

    페이지네이션이 적용된 경우 (DRF 기본 응답 구조) 서버 응답:

    {
      "count": 23,
      "next": "/todo/?page=2",
      "previous": null,
      "results": [
        { "id": 1, "name": "공부하기" },
        { "id": 2, "name": "청소하기" }
      ]
    }
    

    이건 data.results가 배열입니다. 조건 Array.isArray(data.results)true가 되므로 return data.results; 실행됨

    잘못된 응답일 경우:

    {
      "error": "잘못된 요청입니다."
    }
    

    위 조건들을 모두 통과하지 못하면 return [] → 빈 배열 반환


    데이터가 들어오는 방식을 확인한다. [참고] APIView (raw list 반환)

    function extractTodoArray(data) {
      if (Array.isArray(data)) return data;
      if (Array.isArray(data.results)) return data.results;
      return [];
    }
    

    순수한 @api_view(['GET'])APIView 를 써서 직접 return Response(my_queryset_list) 처럼 리스트 자체를 그대로 내보내면, 최종 JSON이

    [
      { "id": 1, "name": "...", … },
      { "id": 2, "name": "...", … }
    ]
    

    이런 형태로 옵니다. 따라서 response.data 가 곧 배열이고, extractTodoArray 첫 줄에서 바로 잡아낼 수 있는 거죠.

    ViewSet (pagination + 메타데이터 포함)

    function extractTodoArray(data) {
      if (Array.isArray(data.data))    return data.data;
      if (Array.isArray(data.results)) return data.results;
      return [];
    }
    

    ModelViewSet 등에서 페이징(pagination) 을 사용하도록 설정해 두면, DRF는 기본적으로

    {
      "count": 42,
      "next": "http://…?page=2",
      "previous": null,
      "results": [ {…}, {…}, … ]
    }
    

    와 같이 항목 배열을 감싸는 메타 필드를 같이 붙여줍니다.
    따라서 실제 Todo 객체들은 data.results(기본 PageNumberPagination) 또는 여러분이 커스터마이징했다면 data.data 같은 다른 키로 옮겨져 있게 되고, 이걸 골라 내려면 두 번째 extractTodoArray 형태가 필요합니다.


    8. Todo 목록을 웹페이지에 화면으로 출력해주는 핵심 함수입니다.

    function renderTodoList(todos) {
        const container = document.querySelector('.todocontainer');
        container.innerHTML = '';
        todos.forEach(todo => container.appendChild(createTodoElement(todo)));
    }
    

    이 함수는 todos 배열을 받아서,
    HTML .todocontainer 요소 안에 있는 기존 내용을 모두 지우고,
    새롭게 할 일 항목들을 하나씩 만들어서 화면에 붙이는 함수입니다.

    const container = document.querySelector('.todocontainer'); 할 일 목록이 표시될 HTML 영역을 선택합니다

    .todocontainer는 보통 아래와 같이 정의되어 있겠죠:

    <div class="todocontainer"></div>
    

    container.innerHTML = ''; 기존에 있던 HTML 내용을 모두 비웁니다 (초기화). 즉, 이전에 표시된 할 일들을 모두 지우고 새로 그릴 준비를 하는 것이죠 왜? → loadTodoList(page)로 다시 불러올 때 겹치지 않게!

    todos.forEach(todo => container.appendChild(createTodoElement(todo))); todos는 서버에서 받아온 할 일 객체들의 배열입니다.

    [
      { "id": 1, "name": "공부하기", "complete": false },
      { "id": 2, "name": "운동하기", "complete": true }
    ]
    

    이 배열을 forEach로 하나씩 순회하며:

    • createTodoElement(todo) 함수를 호출해서
    • 각각의 할 일 항목을 HTML 요소로 만들고
    • .todocontainer 안에 appendChild()로 추가합니다

    9. 위의 함수와 함께 필수로 사용해야 하는 DOM함수

    하나의 Todo 객체를 받아서,
    그 정보를 포함한 HTML 요소를 생성하여 화면에 표시할 수 있도록 반환하는 함수

    function createTodoElement(todo) {
        const div = document.createElement('div');
        div.className = 'todo-item';
        div.innerHTML = `
            <p><strong>Name:</strong> ${todo.name}</p>
            <p><strong>Description:</strong> ${todo.description}</p>
            <p><strong>Complete:</strong> ${todo.complete}</p>
            <p><strong>Completed At:</strong> ${datetimeToString(todo.completed_at)}</p>
            <p><strong>Experience Points:</strong> ${todo.exp}</p>
            <button class="completeBtn">완료</button>
            <hr>
        `;
        return div;
    

    const div = document.createElement('div');<div> 요소를 하나 생성합니다. 이 안에 Todo 하나의 정보를 담을 것입니다.

    div.className = 'todo-item'; 생성한 <div>class="todo-item"을 부여합니다. 이 클래스를 이용해 CSS로 스타일을 줄 수 있습니다.

    .todo-item {
      border: 1px solid #ccc;
      padding: 10px;
      margin-bottom: 10px;
    }
    

    div.innerHTML = `` ; innerHTML`을 사용해 Todo의 내용으로 HTML 구조를 만듭니다. 백틱안에 태그를 만듦니다.

    return div; 위에서 만든 <div> 요소를 반환(return) 하여
    → 다른 함수(renderTodoList)에서 화면에 붙일 수 있도록 합니다.

    datetimeToString(todo.completed_at) 날짜와 시간을 처럼 사람이 읽기 편한 포맷으로 변환됩니다

    // before
    "2025-06-19T09:00:00Z"
    // after
    "2025. 06. 19. 오후 06:00:00"
    

    월요일 여기서 부터 시작


    ✅ 완료버튼을 누르면 밑줄 생기게 하기

    function createTodoElement(todo) {
    	// ....코드생략
      
    	// 완료된 항목에는 'completed' 클래스 추가 → 밑줄 적용
    	if (todo.complete) {
    	 div.classList.add('completed');
    	}
    

    밑줄생기게 css작성하기

    .todo-item.completed {
        text-decoration: line-through;
    }
    

    ✅ 상세보기 이동 및 완료 처리 버튼

    • 목표: 각 항목 클릭 시 상세보기로 이동, 완료 버튼으로 PATCH 요청
    • 테스트: 로그 찍거나 응답 후 리스트 갱신되는지 확인 위의 같은 함수내에 createTodoElement(todo)
        // 클릭 시 상세 페이지 이동
        div.addEventListener('click', () => detailView(todo.id));
    
    • div 요소(각 Todo 아이템)에 클릭 이벤트를 붙입니다.
    • 클릭하면 화살표 함수 (() => detailView(todo.id))가 실행되고,
    • detailView(todo.id) 내부에서
    window.location.href = `/todo/detail/${todo.id}/`;
    

    처럼 브라우저를 해당 URL로 이동시켜 주기 때문에 결과적으로 “리스트 항목을 클릭하면 /todo/detail/<pk>/ 페이지가 열리는” 동작이 됩니다.

    Pk Todo의 상세 페이지로 이동

    // --- 7. 상세보기 이동 --------------------------
    // 특정 Todo의 상세 페이지로 이동
    function detailView(id) {
        window.location.href = `/todo/detail/${id}/`;
    }
    

    위의 같은 함수내에 createTodoElement(todo) 완료 버튼클릭 이벤트

        // 완료 버튼 클릭 시 완료 API 호출
        div.querySelector('.completeBtn')
            .addEventListener('click', e => {
                e.stopPropagation();
                toComplete(todo.id);
            });
    

    HTML 구조가 이렇게 있을 때를 가정해 봅시다:

    <a href="https://example.com" id="link">
      <button id="btn">클릭하세요</button>
    </a>
    

    #child 버튼을 누르면

    • #parent의 캡처링 리스너(있다면)
    • #child의 클릭 리스너
    • #parent의 버블링 리스너
    • documentwindow 순으로
      전부 실행될 수 있습니다.
    document.getElementById('link').addEventListener('click', function () {
      console.log('부모 a 태그 클릭됨');
    });
    
    document.getElementById('btn').addEventListener('click', function () {
      console.log('버튼 클릭됨');
    });
    

    e.stopPropagation() 의 역할

    childButton.addEventListener('click', e => {
      e.stopPropagation();
      // … child 전용 로직만 실행 …
    });
    

    이 한 줄을 쓰면, 버블링 3번 단계에서
    #parent나 그 위 상위 요소의 클릭 핸들러가 절대 호출되지 않습니다.

    <button class="completeBtn">완료</button> 이 클릭되면 div.querySelector('.completeBtn') css밑줄 로 이동 toComplete(todo.id); pk를 호출하여 아래코드와 같이 서버로 접속하고 완료를 전송한다. 성공하면

    function toComplete(id) {
        axiosInstance.patch(`/todo/viewsets/view/${id}/`, { complete: true })
            .then(() => loadTodoList(1))
            .catch(err => console.error('완료 처리 실패:', err));
    }
    

    .then(() => loadTodoList(1)) 다시 화면을 그린다.

    동작흐름:

    [완료 버튼 클릭]
       ↓
    이벤트 발생: completeBtn 클릭
       ↓
    e.stopPropagation() → 부모의 클릭 이벤트(detailView) 막음
       ↓
    toComplete(todo.id) 실행
       ↓
    서버에 PATCH 요청 (complete: true)
       ↓
    성공하면 loadTodoList(1) → 화면 다시 그림
       ↓
    완료된 항목은 "밑줄 스타일" 포함해서 렌더링됨 (CSS)
    

    밑줄은 CSS로 처리됨

    if (todo.complete) {
      div.classList.add('completed');  // ✅ 완료 시 클래스 추가
    }
    

    🔹 update api_view.py
    # todo/api_view.py
    # 수정하기
    class TodoUpdateAPI(APIView):
        def put(self, request, pk):
            try:
                todo = Todo.objects.get(pk=pk)
            except Todo.DoesNotExist:
                return Response({"error":"해당하는 todo가 없습니다."},
            status=status.HTTP_404_NOT_FOUND)
            serializer = TodoSerializer(todo, data=request.data)
            serializer.is_valid(raise_exception=True)
            todo = serializer.save()
            serializer = TodoSerializer(todo)
            return Response(serializer.data)
    
        def patch(self, request, pk):
            try:
                todo = Todo.objects.get(pk=pk)
            except Todo.DoesNotExist:
                return Response({"error":"해당하는 todo가 없습니다."},
            status=status.HTTP_404_NOT_FOUND)
            serializer = TodoSerializer(todo, data=request.data, partial=True)
            serializer.is_valid(raise_exception=True)
            todo = serializer.save()
            serializer = TodoSerializer(todo)
            return Response(serializer.data)
    

    의사코드 : put 요청

    사용자가 특정 Todo를 전체 수정하려고 한다면:
    
    1. 우선 데이터베이스에서 전달받은 pk값을 이용해 Todo 객체를 찾는다.
    
    2. 만약 해당하는 Todo가 존재하지 않는다면:
       - "해당하는 todo가 없습니다."라는 메시지와 함께
       - 404 Not Found 상태 코드를 응답으로 보낸다.
    
    3. Todo가 존재한다면:
       - 요청으로 들어온 데이터를 가지고
       - 기존 Todo 객체에 덮어쓸 준비를 한다.
    
    4. 데이터를 검증한다.
       - 데이터가 올바르지 않으면 예외를 발생시켜 자동으로 에러를 응답한다.
    
    5. 검증에 성공하면 데이터를 저장한다.
       - 즉, Todo 객체를 새 데이터로 업데이트한다.
    
    6. 업데이트된 Todo 객체를 다시 직렬화해서
       - 사용자에게 응답 데이터로 보낸다.
    

    (patch 요청)

    사용자가 특정 Todo를 일부만 수정하려고 한다면:
    
    1. pk값으로 해당 Todo 객체를 데이터베이스에서 찾는다.
    
    2. 만약 해당하는 Todo가 없다면:
       - "해당하는 todo가 없습니다."라는 에러 메시지를
       - 404 상태 코드와 함께 응답으로 보낸다.
    
    3. Todo가 있다면:
       - 요청으로 들어온 데이터 중 일부만 가지고
       - 기존 Todo 객체에 업데이트할 준비를 한다.
    
    4. 데이터를 검증한다.
       - 문제가 있다면 즉시 오류 응답을 보낸다.
    
    5. 문제가 없다면:
       - 해당 필드들만 업데이트해서 저장한다.
    
    6. 저장된 결과를 다시 직렬화해서
       - 사용자에게 응답으로 보낸다.
    

    #todo/views.py
    # 수정
    class TodoUpdateView(UpdateView):
        model = Todo
        fields = ['name', 'description', 'complete', 'exp']
        template_name = "todo/update.html"
        context_object_name = "todo"
        success_url = reverse_lazy('todo_List')
    

    의사코드

    사용자가 기존 Todo 항목을 수정하려고 요청하면 다음과 같이 처리한다:
    
    1. 수정할 Todo 객체는 Todo 모델에서 가져온다.
    
    2. 수정할 수 있는 필드는 다음과 같다:
       - name
       - description
       - complete
       - exp
    
    3. 이 뷰는 HTML 템플릿을 사용해서 폼을 출력한다.
       - 사용할 템플릿 파일은 "todo/update.html"이다.
    
    4. 템플릿 안에서 사용할 객체의 이름은 "todo"로 지정한다.
    
    5. 사용자가 수정 폼을 제출하고 저장에 성공하면:
       - 수정 후 이동할 페이지는 todo_List라는 URL 이름을 가진 페이지이다.
    

    🔹 삭제하기

    todo/api_views.py

    from rest_framework import status
    from rest_framework.views import APIView
    from rest_framework.response import Response
    from .serializers import TodoSerializer
    from .models import Todo
    
    # 삭제하기
    class TodoDeleteAPI(APIView):
        def delete(self, request, pk):
            try:
                todo = Todo.objects.get(pk=pk)
            except Todo.DoesNotExist:
                return Response({"error":"해당하는 todo가 없습니다."},
                status=status.HTTP_404_NOT_FOUND
            )
    
            todo.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
    
    사용자가 특정 Todo 항목을 삭제하려고 요청하면 다음과 같이 처리한다:
    
    1. 우선 요청에서 전달된 pk 값을 기반으로
       데이터베이스에서 해당 Todo 항목을 찾는다.
    
    2. 만약 해당하는 Todo가 존재하지 않는다면:
       - "해당하는 todo가 없습니다."라는 에러 메시지와 함께
       - 404 Not Found 상태 코드를 응답으로 보낸다.
    
    3. Todo가 존재하면:
       - 해당 Todo 객체를 데이터베이스에서 삭제한다.
    
    4. 삭제가 완료되면:
       - 응답으로 아무 내용 없이
       - 204 No Content 상태 코드를 반환한다.
    

    /todo/delete/int:pk/ 특정 Todo 삭제 http://127.0.0.1:8000/todo/api/delete/1/ TodoDeleteAPI `DELETE


    🔹 create.html 의사코드
    1. HTML 문서가 모두 로딩되면 init() 함수를 호출한다.
    
    2. init() 함수는 버튼 클릭 이벤트를 연결하는 함수를 실행한다.
    
    3. 버튼의 id'todoCreate'인 요소를 찾는다.
       - 만약 해당 버튼이 없다면 함수 종료.
       - 버튼이 있다면 클릭 이벤트가 발생했을 때 onCreateClick() 함수가 실행되도록 연결한다.
    
    4. 사용자가 'todoCreate' 버튼을 클릭하면:
       - gatherFormData() 함수를 통해 입력 필드의 값을 수집한다.
       - 수집된 값들을 payload라는 객체에 담는다.
       - 이 payload를 가지고 createTodo(payload)를 실행한다.
    
    5. gatherFormData() 함수는 다음 값을 수집한다:
       - id="name"인 요소의 값을 읽어 name 키에 저장
       - id="description"인 요소의 값을 읽어 description 키에 저장
       - id="complete"인 체크박스의 체크 여부를 complete 키에 저장
       - id="completed_at"인 입력값이 있으면 날짜를 ISO 문자열로 변환하여 completed_at 키에 저장
       - id="exp"인 값이 비어있으면 0, 아니면 숫자로 변환하여 exp 키에 저장
    
    6. 이렇게 수집된 데이터를 가지고 axiosInstance를 통해
       - '/todo/generics/create/' 주소로 POST 요청을 보낸다.
    
    7. 요청에 성공하면 onCreateSuccess()가 실행된다.
       - 이 함수는 브라우저를 '/todo/list/' 주소로 이동시킨다.
    
    8. 요청에 실패하면 onCreateError()가 실행된다.
       - 콘솔에 에러 메시지를 출력하고, 사용자에게 alert 창을 띄운다.
    
    // --- 1. 문서가 완전히 로드되면 초기화 실행 ---
    document.addEventListener('DOMContentLoaded', init);
    
    
    // --- 2. 초기화: 이벤트 바인딩 실행 ----------
    function init() {
      bindUIEvents(); // 버튼에 클릭 이벤트 연결
    }
    
    // --- 3. UI 이벤트 바인딩: 생성 버튼 클릭 이벤트 연결 -----
    
    /*
    클릭, 스크롤, 탭 키 등의 사용자 행동이 발생했을 때 그 행동과 연결된 함수를 실행하도록 연결하는 것" 이게 바로 UI 이벤트 바인딩입니다.
    */
    
    function bindUIEvents() {
      const btn = document.getElementById('todoCreate');
      if (!btn) return;
      btn.addEventListener('click', onCreateClick); // 클릭 시 4번 실행
    }
    
    // --- 4. 생성 버튼 클릭 시 실행되는 핸들러 ---------
    function onCreateClick() {
      const payload = gatherFormData(); 
     // 5번:폼 데이터 수집 gatherFormData()함수에서 각input 요소의 값을 읽음
     
      createTodo(payload); // 6번: 서버에 데이터 전송
    }
    
    // --- 5. 폼 데이터 수집: 입력값을 객체로 변환 ---------
    /*
    constant = 상수 -> 값을 재할당 불가능, 반드시 초기값 필요
    let = 변수 -> 값 재할당 가능, 나중에 할당 가능
    
    파이썬에서는 PI상수라는 강제 규칙이 없습니다. 단지 대문자는 바꾸지 마세요라는 개발자 간의 약속일 뿐입니다.
    그러나 자바스크립트에서는 const를 쓰면 값을 다시 할당하면 에러가 납니다.
    */
    function gatherFormData() {
      let expVal = document.getElementById('exp').value;
      if (expVal === '') expVal = 0; 
      //expVal이 빈 문자열('')이라면, expVal에 기본값 0을 넣어라.
      
      const completedAtInput = document.getElementById('completed_at').value; 
     //HTML 요소 중에서 id="completed_at"인 input 요소를 찾아서 
     //그 입력값(value)을 가져오겠다는 뜻입니다.
    
      const completedAt = completedAtInput ? new Date(completedAtInput).toISOString() : null;
    
      return {
        name:document.getElementById('name').value, 
      // 이 객체 안에 name이라는 키(key) 를 만들고,그 값(value)은 document.getElementById('name').value로 설정해라."
    
        description:document.getElementById('description').value,
        complete:document.getElementById('complete').checked,
        completed_at: completedAt,
        exp:Number(expVal),
      };
    }
    
    // --- 6. API 호출: Todo 생성 요청 ----------
    /*
    axios.post(url, data)는 해당 URL로 data 객체를 서버에 POST 방식으로 전송하라는 의미입니다.
    */
    function createTodo(data) {
      axiosInstance
        .post('/todo/generics/create/', data)
        .then(onCreateSuccess) // 7번: 성공 시 처리
        .catch(onCreateError); // 8번: 실패 시 처리
    }
    
    // --- 7. 생성 성공 핸들러: 목록 페이지로 이동 -----
    function onCreateSuccess(response) {
      // 생성 후 목록 페이지로 이동
      window.location.href = '/todo/list/';
    }
    
    // --- 8. 생성 실패 핸들러: 에러 알림 ----------
    function onCreateError(error) {
      console.error('Todo 생성 실패:', error);
      alert('Todo 생성에 실패했습니다.');
    }
    

    /todo/create/int:pk/ 특정 Todo 수정 http://127.0.0.1:8000/todo/api/create/ TodoCreateAPI POST


    🔹 todo/detail.html 의사코드
    1. 문서가 완전히 로드되면 init() 함수를 실행하라.
    
    2. init() 함수 내부에서 다음 작업을 차례대로 실행하라:
       - 현재 페이지의 URL 경로에서 Todo의 id(pk)를 추출한다.
       - 이 id를 이용해서 해당 Todo 항목의 상세 데이터를 서버에서 불러온다.
       - 데이터를 받아오면 화면에 Todo 상세 정보를 표시한다.
       - 수정 버튼이 존재한다면, 클릭했을 때 수정 페이지로 이동하도록 이벤트를 연결한다.
       - 삭제 버튼이 존재한다면, 클릭했을 때 삭제 여부를 물어보고, 확인하면 삭제 요청을 보내도록 이벤트를 연결한다.
    
    3. URL 경로에서 Todo의 id(pk)를 추출하라:
       - 현재 브라우저 주소창의 경로(`/todo/detail/3/` 등)를 `/` 기준으로 나눠 배열로 만들고,
       - 비어 있지 않은 값들만 필터링한 후,
       - 그 배열의 마지막 요소를 반환하라 (예: "3").
    
    4. 해당 pk를 이용해서 서버에 GET 요청을 보내 Todo 데이터를 받아오라.
       - 주소는 `/todo/viewsets/view/<pk>/` 형식이다.
       - 응답이 성공하면 renderTodoDetail() 함수로 데이터를 넘겨 화면에 표시하라.
       - 실패하면 콘솔에 오류 메시지를 출력하라.
    
    5. 받은 Todo 데이터를 기반으로 HTML 요소를 동적으로 만들어 `.todoDetail` 영역에 삽입하라:
       - 이름(name), 설명(description), 완료 여부(complete),
         완료 날짜(completed_at), 경험치(exp) 등을 HTML 구조로 보여줘라.
    
    6. 수정 버튼(.todoUpdate)을 찾고, 존재하면 다음 동작을 연결하라:
       - 버튼이 클릭되었을 때, `/todo/update/<pk>/` 주소로 페이지를 이동시켜라.
    
    7. 삭제 버튼(.todoDelete)을 찾고, 존재하면 다음 동작을 연결하라:
       - 버튼 클릭 시 사용자에게 정말 삭제할 것인지 확인을 요청하라.
       - 확인을 받으면 `/todo/generics/delete/<pk>/` 주소로 DELETE 요청을 보내라.
       - 삭제 요청이 성공하면 목록 페이지(`/todo/list/`)로 이동하라.
       - 실패하면 alert 창으로 오류를 알려줘라.
    

    todo > urls.py

    from django.urls import path
    from . import views
    from . import api_views
    
    urlpatterns = [
        # APIView  
        api_views.TodoDeleteAPI.as_view(), name="todo_api_delete"),
    ]
    
    {% extends "base.html" %}
    {% load static %}
    {% block content %}
    
    <div class="todoDetail"></div>
    <div class="btnList">
        <button class="todoUpdate">수정</button>
        <button class="todoDelete">삭제</button>
    </div>
    
    <script src="{% static 'js/axios.min.js' %}"></script>
    <script>
    
    // --- 공통 Axios 인스턴스 정의 ----------------
    // const axiosInstance = axios.create({
    //    baseURL: '/',    
    //    timeout: 5000,
    //    headers: { 'Content-Type': 'application/json' }
    // }); 
    
      
    // --- 1. 초기화 -------------------------------
    document.addEventListener('DOMContentLoaded', init);
    
    // --- 2.
    function init() {
        const pk = getTodoId();
        loadTodoDetail(pk);
        bindUpdateButton(pk);
        bindDeleteButton(pk);
    }
    
    // --- 3. URL에서 PK 추출 -----------------------
    function getTodoId() {
        // 예: /todo/detail/3/ → ["todo","detail","3"] → pop() → "3"
        return window.location.pathname
                   .split('/')
                   .filter(Boolean)
                   .pop();
    }
    
    // --- 4. 상세 정보 로드 & 렌더링 ---------------
    function loadTodoDetail(pk) {
        // 수정된 API 경로: TodoRetrieveAPI → /todo/api/retrieve/<pk>/
        axiosInstance
            .get(`/todo/viewsets/view/${pk}/`)      // ← 주소 수정: retrieve endpoint 사용
            .then(res => renderTodoDetail(res.data))
            .catch(err => console.error('상세 조회 실패:', err));
    }
    
    // --- 5.
    function renderTodoDetail(todo) {
        const container = document.querySelector('.todoDetail');
        container.innerHTML = `
            <div class="todo-item">
                <p><strong>Name:</strong> ${todo.name}</p>
                <p><strong>Description:</strong> ${todo.description}</p>
                <p><strong>Complete:</strong> ${todo.complete}</p>
                <p><strong>Completed At:</strong> ${todo.complete_at}</p>
                <p><strong>Experience Points:</strong> ${todo.exp}</p>
            </div>
        `;
    }
    
    // --- 6. 수정 버튼 바인딩 ----------------------
    function bindUpdateButton(pk) {
        const btn = document.querySelector('.todoUpdate');
        if (!btn) return;
        btn.addEventListener('click', () => {
            // 수정 페이지 이동 (백엔드 뷰: TodoUpdateAPI는 API용, 프론트엔드 URL은 별도)
            window.location.href = `/todo/update/${pk}/`;
        });
    }
    
    // --- 7. 삭제 버튼 바인딩 ----------------------
    function bindDeleteButton(pk) {
        const btn = document.querySelector('.todoDelete');
        if (!btn) return;
        btn.addEventListener('click', () => {
            if (!confirm('정말 해당 Todo를 삭제하시겠습니까?')) return;
            // 수정된 API 경로: TodoDeleteAPI → /todo/api/delete/<pk>/
            axiosInstance
                .delete(`/todo/generics/delete/${pk}/`)  // ← 주소 수정: delete endpoint 사용
                .then(() => window.location.href = '/todo/list/')
                .catch(() => alert('Todo 삭제에 실패했습니다.'));
        });
    }
    </script>
    {% endblock %}
    

    /todo/detail/int:pk/ 특정 Todo 수정 http://127.0.0.1:8000/todo/api/retrieve/<int:pk> TodoRetrieveAPI GET


    🔹 todo/update.html 의사코드
    1. 문서가 완전히 로드되면 init() 함수를 실행한다.
    
    2. init() 함수에서 다음과 같은 초기 작업을 진행한다:
       - 브라우저 주소(URL)에서 pk(할 일의 고유 ID)를 추출한다.
       - 해당 pk를 이용해서 서버에서 기존 Todo 데이터를 불러오고, HTML 폼에 값을 채운다.
       - 수정 버튼이 클릭될 수 있도록 이벤트를 연결한다.
    
    3. getTodoId():
       현재 브라우저 주소 경로(`/todo/update/5/`)에서 마지막 요소("5")를 추출한다.
       이 값을 통해 어떤 Todo를 수정할지 결정한다.
    
    4. loadTodoIntoForm(pk):
       주어진 pk에 해당하는 Todo 데이터를 서버에서 가져오기 위해 GET 요청을 보낸다.
       응답이 성공적으로 오면 populateForm() 함수를 호출해 HTML 폼에 기존 데이터를 채운다.
       요청에 실패하면 콘솔에 에러를 출력한다.
    
    5. populateForm(todo):
       불러온 Todo 데이터를 바탕으로 HTML `<input>` 요소에 값들을 채워 넣는다.
       - 이름(name)과 설명(description)은 `.value`에 넣고,
       - 완료 여부(complete)는 체크박스이므로 `.checked`에 넣는다.
       - 경험치(exp)는 숫자 입력창의 `.value`로 설정한다.
    
    6. bindUpdateButton(pk):
       수정 버튼(`#todoUpdate`)을 찾아서 클릭 이벤트를 연결한다.
       클릭 시 handleUpdate(pk) 함수를 실행하게 만든다.
    
    7. handleUpdate(pk):
       현재 폼에 입력된 값들을 객체(payload)로 수집한다.
       - 이 값들을 JSON 형식으로 PATCH 요청을 보낸다.
       - 주소는 `/todo/generics/update/<pk>/` 형식의 수정 API다.
       - 성공하면 상세보기 페이지(`/todo/detail/<pk>/`)로 이동한다.
       - 실패하면 콘솔에 에러 로그를 출력하고, 사용자에게 실패 alert을 띄운다.
    
    8. redirectToDetail(pk):
       수정이 완료되면 해당 Todo의 상세 페이지(`/todo/detail/<pk>/`)로 브라우저를 이동시킨다.
    
    {% extends "base.html" %}
    {% load static %}
    {% block content %}
    
    <div class="container">
      <h2>Update Your Todo</h2>
      <div>
        <label for="name">Name:</label>
        <input type="text" name="name" id="name">
      </div>
    
      <div>
        <label for="description">Description:</label>
        <textarea name="description" id="description"></textarea>
      </div>
    
      <div>
        <label for="complete">Complete:</label>
        <input type="checkbox" name="complete" id="complete">
      </div>
    
      <div>
        <label for="exp">Experience Points:</label>
        <input type="number" name="exp" id="exp" min="0">
      </div>
    
      <button type="button" id="todoUpdate">Update</button>
    </div>
    
    <script src="{% static 'js/axios.min.js' %}"></script>
    <script>
    
    
    // --- 1. 초기화 -------------------------------
    document.addEventListener('DOMContentLoaded', init);
    
    // --- 2. 초기화
    function init() {
      const pk = getTodoId();
      loadTodoIntoForm(pk);
      bindUpdateButton(pk);
    }
    
    // --- 3. URL에서 PK 추출 ------------------------
    function getTodoId() {
      // 예: /todo/update/5/ → ["todo","update","5"] → pop() → "5"
      return window.location.pathname.split('/').filter(Boolean).pop();
    }
    
    // --- 4. 기존 Todo 데이터 로드 & 폼 채우기 -----
    function loadTodoIntoForm(pk) {
      // 상세조회 API 호출 (TodoRetrieveAPI)
      axiosInstance
        .get(`/todo/generics/retrieve/${pk}/`)            // ← retrieve 엔드포인트
        .then(res => populateForm(res.data))
        .catch(err => console.error('로딩 실패:', err));
    }
    
    // --- 5.
    function populateForm(todo) {
      document.getElementById('name').value        = todo.name;
      document.getElementById('description').value = todo.description;
      document.getElementById('complete').checked  = todo.complete;
      document.getElementById('exp').value         = todo.exp;
    }
    
    // --- 6. 업데이트 버튼 클릭 바인딩 -------------
    function bindUpdateButton(pk) {
      const btn = document.getElementById('todoUpdate');
      if (!btn) return;
      btn.addEventListener('click', () => handleUpdate(pk));
    }
    
    // --- 7. 업데이트 요청 처리 --------------------
    function handleUpdate(pk) {
      const payload = {
        name:        document.getElementById('name').value,
        description: document.getElementById('description').value,
        complete:    document.getElementById('complete').checked,
        exp:         Number(document.getElementById('exp').value),
      };
      // 수정 API 호출 (TodoUpdateAPI)
      axiosInstance
        .patch(`/todo/generics/update/${pk}/`, payload)   
        // ← update 엔드포인트
        
        .then(() => redirectToDetail(pk))
        .catch(err => {
          console.error('수정 실패:', err);
          alert('Todo 수정에 실패했습니다.');
        });
    }
    
    // --- 8. 수정 후 상세 페이지로 이동 -------------
    function redirectToDetail(pk) {
      window.location.href = `/todo/detail/${pk}/`;
    }
    </script>
    {% endblock %}
    

    /todo/update/int:pk/ 특정 Todo 수정 http://127.0.0.1:8000/todo/api/update/1/ TodoUpdateAPI PATCH or PUT


    탬플릿

    templates/base.html

    {% load static %} 
    <!DOCTYPE html>
    <html lang="ko">
    <head>
      {% include "head.html" %}
    </head>
    <body>
        {% include 'header.html' %} #추가
    	{% block content %}{% endblock %}
    	{% include 'footer.html' %} #추가
    
      <!-- Global Scripts -->
      <script src="{% static 'js/getCookie.js' %}"></script>
      <script src="{% static 'js/axiosInstance.js' %}"></script>
      <script src="{% static 'js/utils.js' %}"></script>
    
      <!-- 페이지별 개별 스크립트 삽입 위치 -->
      {% block scripts %}{% endblock %}
    
    </body>
    </html>
    

    templates/head.html

    {% load static %}
    <title>{% block title %}DjangoCourse{% endblock %}</title>
    
    <!-- CSS -->
    <link rel="stylesheet" href="{% static 'css/styles.css' %}">
    
    <!-- Axios -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    

    templates/header.html

    {% load static %}
    <div class="header">
      <h1>Todo List</h1>
      <div class="user-actions">
        <a href="{% url 'todo_List' %}" class="nav-link">📋 목록</a>
    
        {% if user.is_authenticated %}
          <!-- 🔓 로그아웃 버튼 (id 추가) -->
          <button id="logoutBtn" class="nav-link logout">🔓 로그아웃</button>
        {% else %}
          <!-- 🔒 로그인은 여전히 DRF 기본 로그인 뷰 사용 -->
          <a
            href="{% url 'rest_framework:login' %}?next={% url 'todo_List' %}"
            class="nav-link login"
          >🔒 로그인</a>
        {% endif %}
      </div>
    </div>
    
    <script>
    document.addEventListener("DOMContentLoaded", () => {
      const logoutBtn = document.getElementById("logoutBtn");
    
      if (logoutBtn) {
        logoutBtn.addEventListener("click", async () => {
          try {
            await axiosInstance.post("/todo/api/custom-logout/");
            window.location.href = "{% url 'todo_List' %}";
          } catch (err) {
            console.error("로그아웃 실패:", err);
            alert("로그아웃 중 오류가 발생했습니다.");
          }
        });
      }
    });
    </script>
    

    templates/footer.html

    {% load static %}
        <div class = "footer">
          <a href="https://github.com/handgonpo">Contact Me: handgonpo@naver.com</a>
        </div>
    

    utils.js
    function datetimeToString(datetime){
        if (!datetime) return "-";
        const date = new Date(datetime);
        return date.toLocaleString("ko-KR", { timeZone: "Asia/Seoul" });
    }
    

    서버에서 받은 "날짜와 시간 데이터"를
    사람이 읽기 쉬운 한국식 날짜/시간 형식으로 바꿔주는 함수입니다


    TOP
    preload preload